{*******************************************************************************
* The contents of this file are used with permission, subject to the Mozilla   *
* Public License Version 1.1 (the "License"); you may not use this file except *
* in compliance with the License. You may obtain a copy of the License at      *
* http://www.mozilla.org/MPL/                                                  *
*                                                                              *
* Software distributed under the License is distributed on an "AS IS" basis,   *
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for *
* the specific language governing rights and limitations under the License.    *
*                                                                              *
* The Original Code is distributed as part of the "mediate" product and is     *
* Copyright (C) @home media limited. All Rights Reserved.                      *
*                                                                              *
* Email: support@athomemedia.co.uk                                             *
* Web:   http://www.athomemedia.co.uk                                          *
*******************************************************************************}

unit UIManager;

interface

uses Windows, Classes, Graphics, AHMTypes, AHMConsts, Screens, UIObjects,
     BaseControls, Containers, Diagnostics, ScreenSaver, ConsoleManager,
     Gestures, Bitmaps, Places;

const
  MOUSE_HIDE_DELAY = 5000;    // 5 second delay before hiding mouse
  MOUSE_JITTER_THRESHOLD = 5; // 5 pixel movement threshold before showing mouse
  MAX_HISTORY_LIMIT = 10; // maximum no. of screen history movements to record

  TEXT_CUSTOM_DATE_FORMAT = 'system.dateformat';
  TEXT_CUSTOM_TIME_FORMAT = 'system.timeformat';

  PREVIEW_CONTROL_NAME = 'designer.preview';

type
  TAHMScreenStack = class
  private
    FScreenStack: TStringList;
    function GetScreenObject(ScreenName: String): TAHMScreenLayout;
    function GetStackHeight: Integer;
    function GetScreen(Index: Integer): TAHMScreenLayout;
    function GetScreenName(Index: Integer): String;
    function GetTopScreen: TAHMScreenLayout;
    function GetTopScreenName: String;
  protected
    property TopScreen: TAHMScreenLayout read GetTopScreen;
    property TopScreenName: String read GetTopScreenName;
    property StackHeight: Integer read GetStackHeight;
    property Screens[Index: Integer]: TAHMScreenLayout read GetScreen;
    property ScreenNames[Index: Integer]: String read GetScreenName;
    function PushScreen(ScreenName: String): Boolean; overload;
    function PushScreen(Screen: TAHMScreenLayout): Boolean; overload;
    function PopScreen(ScreenName: String): Boolean; overload;
    function PopScreen(Screen: TAHMScreenLayout): Boolean; overload;
    function FindScreen(Ignore: TAHMScreenLayout): TAHMScreenLayout;
    procedure ClearStack;
    procedure CleanStack;
  public
    constructor Create;
    destructor Destroy; override;
  end;

  TAHMScreenHistory = class
  private
    FScreens: TStringList;
    FIndex: Integer;
  protected
    procedure Visited(ScreenName: String);
    procedure Closed(ScreenName: String);
    procedure Clear;
    function Backwards: String;
    function Forwards: String;
  public
    constructor Create;
    destructor Destroy; override;
  end;

  TAHMUIRenderer = class
  public
    procedure Render; virtual; abstract;
  end;

  TAHMUIManager = class
  private
    FCurrentScreen: String;
    FResourceURL: String;
    FLoading: Boolean;
    FControlBorders: Boolean;
    FContainerBorders: Boolean;
    FScreenInitialised: Boolean;
    FDisplayDiagnostics: Boolean;
    FMouseCursor: TAHMMouseCursor;
    FMouseLastX: Integer;
    FMouseLastY: Integer;
    FMouseScreenX: Single;
    FMouseScreenY: Single;
    FMouseVirtualX: Integer;
    FMouseVirtualY: Integer;
    FMouseActive: Boolean;
    FMouseHideTimer: TAHMTimestamp;
    FDesignerRenderer: TAHMUIRenderer;
    FDiagnosticRenderer: TAHMDiagnostics;
    FScreenSaver: TAHMScreenSaver;
    FScreenStack: TAHMScreenStack;
    FScreenHistory: TAHMScreenHistory;
    FGestureManager: TAHMGestureManager;
    FOnDescriptor: TNotifyEvent;
    FPreviewObject: TAHMUIObject;
    FPreviewControl: TAHMControl;
    FPreviewBitmap: TAHMBitmap;
    FPreviewPlace: TAHMPlace;
  protected
    function GetConsole: TAHMConsoleManager;
    function GetScreen(ScreenName: String): TAHMScreenLayout;
    function GetCurrentScreen: TAHMScreenLayout;
    function GetFocusControl: TAHMInteractiveControl;
    procedure SetCurrentScreen(Screen: TAHMScreenLayout);
    procedure HandleScreenChange(OldScreenName, NewScreenName: String);
    procedure InitialiseScreen;
    procedure SetMouseCursor(Cursor: TAHMMouseCursor);
    procedure ConfigureMouse(Active: Boolean);
    function CommonMouseHandler(const X, Y: Integer; Wake: Boolean): Boolean;
  public
    constructor Create;
    destructor Destroy; override;
    property CurrentScreen: TAHMScreenLayout read GetCurrentScreen;
    property FocusControl: TAHMInteractiveControl read GetFocusControl;
    property MouseCursor: TAHMMouseCursor read FMouseCursor write SetMouseCursor;
    property DrawControlBorders: Boolean read FControlBorders;
    property DrawContainerBorders: Boolean read FContainerBorders;
    property DisplayDiagnostics: Boolean read FDisplayDiagnostics write FDisplayDiagnostics;
    property DisplayDesigner: TAHMUIRenderer read FDesignerRenderer write FDesignerRenderer;
    property ScreenSaver: TAHMScreenSaver read FScreenSaver;
    property ResourceURL: String read FResourceURL;
    property ScreenMouseX: Integer read FMouseLastX;
    property ScreenMouseY: Integer read FMouseLastY;
    property VirtualMouseX: Integer read FMouseVirtualX;
    property VirtualMouseY: Integer read FMouseVirtualY;
    property DebugConsole: TAHMConsoleManager read GetConsole;
    property PreviewObject: TAHMUIObject read FPreviewObject write FPreviewObject;
    property OnDescriptorLoaded: TNotifyEvent read FOnDescriptor write FOnDescriptor;
    procedure InitialiseTheme;
    procedure LoadDescriptor(GroupName: String);
    procedure JumpToBaseScreen;
    procedure JumpToScreen(ScreenName: String);
    procedure OverlayScreen(ScreenName: String);
    procedure CloseScreen(ScreenName: String);
    procedure InitialiseDescriptor;
    procedure UpdateUI;
    procedure RenderUI;
    procedure RenderPreview(UIObject: TAHMUIObject; Handle: HWND);
    procedure InvalidateUIObject(UIObject: TAHMUIObject);
    procedure DeleteUIObject(UIObject: TAHMUIObject);
    function GetPreviewControl(ControlClass: TAHMControlClass; NewInstance: Boolean = False): TAHMControl;
    function GetControl(ControlName: String; OnScreenOnly: Boolean): TAHMControl;
    function GetContainer(ContainerName: String; OnScreenOnly: Boolean): TAHMContainer;
    procedure DisplayError(ErrorText, ErrorDescription: String);
    function ProcessRemoteCommand(KeyCode: Word): Boolean;
    function ProcessKeyPress(KeyCode: Word; Shift: TShiftState): Boolean;
    function ProcessMouseDown(X, Y: Integer): Boolean;
    function ProcessMouseMove(X, Y: Integer; Down: Boolean): Boolean;
    function ProcessMouseUp(X, Y: Integer): Boolean;
    function ProcessMouseWheel(Delta: Integer): Boolean;
    function UpdateMousePosition(X, Y: Integer): Boolean;
    function RemoveAbstractControl(Remove: TAHMAbstractControl): TAHMUIObject;
    function FindAbstractControl(X, Y: Integer; Ignore: TAHMAbstractControl): TAHMAbstractControl;
    function FindControl(X, Y: Integer; Ignore: TAHMControl): TAHMControl;
    function FindContainer(X, Y: Integer; Ignore: TAHMContainer): TAHMContainer;
    function FindScreen(X, Y: Integer; Ignore: TAHMScreenLayout): TAHMScreenLayout;
  end;


implementation

uses SysUtils, ApplicationManager, VisualDescriptor, InterfaceDescriptor,
     LanguageDescriptor, Languages, Screen, MetaConsts, Actions, Timing,
     Shapes, Colours, ThemeManager, UIControls, Utilities, TagNames, Logging;


// TAHMScreenStack

constructor TAHMScreenStack.Create;
begin
  inherited Create;

  FScreenStack := TStringList.Create;
end;

destructor TAHMScreenStack.Destroy;
begin
  FScreenStack.Free;

  inherited Destroy;
end;

function TAHMScreenStack.GetScreenObject(ScreenName: String): TAHMScreenLayout;
begin
  Result := ApplicationManagerInstance.ThemeManager.InterfaceDescriptor.ScreenList.GetScreenLayout(ScreenName);
end;

function TAHMScreenStack.GetStackHeight: Integer;
begin
  Result := FScreenStack.Count;
end;

function TAHMScreenStack.GetScreen(Index: Integer): TAHMScreenLayout;
var
  sName: String;
begin
  sName := GetScreenName(Index);
  if sName <> '' then
    Result := GetScreenObject(sName)
  else
    Result := nil;
end;

function TAHMScreenStack.GetScreenName(Index: Integer): String;
begin
  if (Index >= 0) and (Index < FScreenStack.Count) then
    Result := FScreenStack[Index]
  else
    Result := '';
end;

function TAHMScreenStack.GetTopScreen: TAHMScreenLayout;
begin
  if FScreenStack.Count > 0 then
    Result := GetScreen(Pred(FScreenStack.Count))
  else
    Result := nil;
end;

function TAHMScreenStack.GetTopScreenName: String;
begin
  if FScreenStack.Count > 0 then
    Result := FScreenStack[Pred(FScreenStack.Count)]
  else
    Result := '';
end;

function TAHMScreenStack.PushScreen(ScreenName: String): Boolean;
var
  Screen: TAHMScreenLayout;
begin
  Result := False;

  // Check passed screen is not already in stack
  if FScreenStack.IndexOf(ScreenName) = -1 then
  begin
    Screen := GetScreenObject(ScreenName);
    if Assigned(Screen) then
    begin
      // Screen is available, so we can add it to stack
      FScreenStack.Add(ScreenName);
      Result := True;

      // Ensure screens onshow actions get fired
      Screen.Visible := True;
    end;
  end;
end;

function TAHMScreenStack.PushScreen(Screen: TAHMScreenLayout): Boolean;
begin
  Result := Assigned(Screen) and PushScreen(Screen.Name);
end;

function TAHMScreenStack.PopScreen(ScreenName: String): Boolean;
var
  i: Integer;
  Screen: TAHMScreenLayout;
begin
  Result := False;

  // Check passed screen is currently in stack
  i := FScreenStack.IndexOf(ScreenName);
  if i <> -1 then
  begin
    FScreenStack.Delete(i);
    Result := True;

    // Ensure screens onhide actions get fired
    Screen := GetScreenObject(ScreenName);
    if Assigned(Screen) then Screen.Visible := False;
  end;
end;

function TAHMScreenStack.PopScreen(Screen: TAHMScreenLayout): Boolean;
begin
  Result := Assigned(Screen) and PopScreen(Screen.Name);
end;

function TAHMScreenStack.FindScreen(Ignore: TAHMScreenLayout): TAHMScreenLayout;
var
  i: Integer;
  oScreen: TAHMScreenLayout;
begin
  // Set default result - no screen found
  Result := nil;

  // Iterate through screens in reverse draw order - i.e. check topmost first
  for i := Pred(FScreenStack.Count) downto 0 do
  begin
    oScreen := GetScreen(i);

    // Skip all screens until we find our ignore screen
    if Ignore <> nil then
    begin
      if Ignore = oScreen then Ignore := nil;
      Continue;
    end;

    // First screen that we don't skip is the one we want
    Result := oScreen;
    Break;
  end;
end;

procedure TAHMScreenStack.ClearStack;
begin
  FScreenStack.Clear;
end;

procedure TAHMScreenStack.CleanStack;
var
  i: Integer;
  sName: String;
begin
  // Remove any screens from stack that are no longer loaded
  for i := Pred(FScreenStack.Count) downto 0 do
  begin
    sName := FScreenStack[i];
    if GetScreenObject(sName) = nil then
      FScreenStack.Delete(i);
  end;
end;

//----------------------------------------------------------------------------//
// TAHMScreenHistory

constructor TAHMScreenHistory.Create;
begin
  inherited;

  FScreens := TStringList.Create;
end;

destructor TAHMScreenHistory.Destroy;
begin
  FScreens.Free;

  inherited;
end;

procedure TAHMScreenHistory.Visited(ScreenName: String);
begin
  if ScreenName <> '' then
  begin
    // Truncate any history items after current index (can't go forwards now)
    while FScreens.Count > Succ(FIndex) do FScreens.Delete(Succ(FIndex));

    // Truncate old history
    while FScreens.Count > MAX_HISTORY_LIMIT do FScreens.Delete(0);

    // Add visited screen to history
    FIndex := FScreens.Add(ScreenName);
  end;
end;

procedure TAHMScreenHistory.Closed(ScreenName: String);
var
  i: Integer;
begin
  if ScreenName <> '' then
  begin
    // Remove last instance of this screen and all later items
    for i := Pred(FScreens.Count) downto 0 do
      if FScreens[i] = ScreenName then
      begin
        while FScreens.Count > i do
          FScreens.Delete(i);
        FIndex := FScreens.Count;
        Break;
      end;
  end;
end;

procedure TAHMScreenHistory.Clear;
begin
  FScreens.Clear;
  FIndex := 0;
end;

function TAHMScreenHistory.Backwards: String;
begin
  // Set default result - can't go backwards
  Result := '';

  if FIndex > 0 then
  begin
    Dec(FIndex);
    Result := FScreens[FIndex];
  end;
end;

function TAHMScreenHistory.Forwards: String;
begin
  // Set default result - can't go forwards
  Result := '';

  if FIndex < Pred(FScreens.Count) then
  begin
    Inc(FIndex);
    Result := FScreens[FIndex];
  end;
end;


//----------------------------------------------------------------------------//
// TAHMUIManager

constructor TAHMUIManager.Create;
begin
  inherited Create;

  // Create diagnostic renderer & screen saver objects
  FDiagnosticRenderer := TAHMDiagnostics.Create;
  FScreenSaver := TAHMScreenSaver.Create;
  FScreenStack := TAHMScreenStack.Create;
  FScreenHistory := TAHMScreenHistory.Create;
  FGestureManager := TAHMGestureManager.Create;

  // Configure UI resource path
  FResourceURL := ParentFolder(ApplicationPath) + RESOURCE_FOLDER + PathDelim;
end;

destructor TAHMUIManager.Destroy;
begin
  // Free our diagnostic renderer & screen saver
  FDiagnosticRenderer.Free;
  FScreenSaver.Free;
  FScreenStack.Free;
  FScreenHistory.Free;
  FGestureManager.Free;
  FPreviewControl.Free;
  FPreviewBitmap.Free;
  FPreviewPlace.Free;

  inherited Destroy;
end;

function TAHMUIManager.GetConsole: TAHMConsoleManager;
begin
  Result := FDiagnosticRenderer.Console;
end;

function TAHMUIManager.GetScreen(ScreenName: String): TAHMScreenLayout;
begin
  if ScreenName = '' then
    Result := nil
  else
    Result := ApplicationManagerInstance.ThemeManager.InterfaceDescriptor.ScreenList.GetScreenLayout(ScreenName);
end;

function TAHMUIManager.GetCurrentScreen: TAHMScreenLayout;
begin
  Result := GetScreen(FCurrentScreen);
end;

function TAHMUIManager.GetFocusControl: TAHMInteractiveControl;
begin
  if GetCurrentScreen <> nil then Result := GetCurrentScreen.FocusControl
                             else Result := nil;
end;

procedure TAHMUIManager.SetCurrentScreen(Screen: TAHMScreenLayout);
begin
  if CurrentScreen <> Screen then
  begin
    // Ensure new screen is at top of screen stack
    FScreenStack.PopScreen(FCurrentScreen);
    FScreenStack.PushScreen(Screen);

    // Fire appropriate actions when we switch current screen
    if Assigned(Screen) then HandleScreenChange(FCurrentScreen, Screen.Name)
                        else HandleScreenChange(FCurrentScreen, '');

    // Clean stack to remove any other stale screens
    FScreenStack.CleanStack;

    // Store new screen name
    if Assigned(Screen) then FCurrentScreen := Screen.Name
                        else FCurrentScreen := '';

    // And now perform standard screen setup stuff
    InitialiseScreen;
  end;
end;

procedure TAHMUIManager.HandleScreenChange(OldScreenName, NewScreenName: String);
var
  OldScreen, NewScreen: TAHMScreenLayout;
begin
  // Lookup relevant screen layouts
  OldScreen := GetScreen(OldScreenName);
  NewScreen := GetScreen(NewScreenName);

  // Did we change screens really?
  if OldScreen <> NewScreen then
  begin
    if Assigned(OldScreen) then OldScreen.Focussed := False;
    if Assigned(NewScreen) then NewScreen.Focussed := True;

    // Flush any outstanding actions for our previous screen
    if Assigned(OldScreen) then OldScreen.UpdateActions;
  end;
end;

procedure TAHMUIManager.InitialiseScreen;
begin
  if FCurrentScreen <> '' then
    with CurrentScreen do
    begin
      // Set initial focus control on our screen
      if SaveControl <> nil then
        FocusControl := SaveControl
      else
        FocusControl := DefaultControl;

      // Apply meta values for this screen
      MetaValues.Apply;

      // Set flag to run screen initialisation actions
      FScreenInitialised := False;
    end;
end;

procedure TAHMUIManager.SetMouseCursor(Cursor: TAHMMouseCursor);
begin
  FMouseCursor := Cursor;
  ConfigureMouse(False);
end;

procedure TAHMUIManager.ConfigureMouse(Active: Boolean);
begin
  // Configure mouse cursor display
  case FMouseCursor of
    mcShow: ScreenInstance.MouseVisible := True;
    mcHide: ScreenInstance.MouseVisible := False;
    mcAuto: ScreenInstance.MouseVisible := Active;
  end;

  // If mouse was just shown then start timer for autohiding
  if Active and (FMouseCursor = mcAuto) then
    FMouseHideTimer := TimerInstance.CurrentTimestamp;

  // Record mouse activity status
  FMouseActive := Active;
end;

procedure TAHMUIManager.InitialiseTheme;
var
  sDate, sTime: String;
begin
  // Hide mousey
  ConfigureMouse(False);

  // Initialise custom descriptor
  InitialiseDescriptor;

  // Setup date & time formats
  sDate := LanguageManagerInstance.Translate(TEXT_CUSTOM_DATE_FORMAT);
  sTime := LanguageManagerInstance.Translate(TEXT_CUSTOM_TIME_FORMAT);
  if sDate <> '' then TimerInstance.DateFormat := sDate;
  if sTime <> '' then TimerInstance.TimeFormat := sTime;
end;

procedure TAHMUIManager.LoadDescriptor(GroupName: String);
var
  CurrDescName: String;
begin
  // Don't load the base descriptor - should be loaded using LoadTheme instead!
  if GroupName = BASE_DESCRIPTOR_PREFIX then Exit;

  // Set loading state so we don't process UI invalidation
  FLoading := True;
  try
    // Save original UI state
    CurrDescName := ApplicationManagerInstance.ThemeManager.ThemeGroup;
    try
      // Load specified custom descriptor and switch focus to default screen
      if ApplicationManagerInstance.ThemeManager.LoadDescriptor(GroupName) then
      begin
        // Initialise our new descriptor
        InitialiseDescriptor;

        // Signal event notification
        if Assigned(FOnDescriptor) then FOnDescriptor(Self);
      end
      else
      begin
        LogWarn('Unknown descriptor requested for loading: ' + GroupName);
        // Failed to initialise, so reload original descriptor
        if CurrDescName <> '' then
        begin
          ApplicationManagerInstance.ThemeManager.LoadDescriptor(CurrDescName);
          InitialiseDescriptor;
        end;
      end;
    except
      // Descriptor loading went horribly wrong, log suitable exception
      on E: Exception do raise EThemeException.Create('Error loading theme descriptor ' + GroupName +
                                                ':' + sLineBreak + E.Message);
    end;
  finally
    FLoading := False;
  end;
end;

procedure TAHMUIManager.JumpToBaseScreen;
var
  ScreenList: TAHMScreenLayoutList;
begin
  // Get screens from base interface descriptor
  ScreenList := ApplicationManagerInstance.ThemeManager.InterfaceDescriptor.ScreenList;
  if ScreenList.BaseList <> nil then ScreenList := TAHMScreenLayoutList(ScreenList.BaseList);

  // Point to first screen in base descriptor
  FScreenHistory.Clear;
  if ScreenList <> nil then
    SetCurrentScreen(ScreenList.GetDefaultLayout)
  else
    SetCurrentScreen(nil);
  FScreenHistory.Visited(FCurrentScreen);
end;

procedure TAHMUIManager.JumpToScreen(ScreenName: String);
var
  NewScreen: TAHMScreenLayout;
begin
  if FCurrentScreen <> ScreenName then
  begin
    // Do we want to go forwards or backwards through history?
    if ScreenName = ATTR_VAL_BACKWARDS then
      ScreenName := FScreenHistory.Backwards
    else if ScreenName = ATTR_VAL_FORWARDS then
      ScreenName := FScreenHistory.Forwards
    else FScreenHistory.Visited(ScreenName);

    NewScreen := GetScreen(ScreenName);
    if NewScreen <> nil then
      SetCurrentScreen(NewScreen)
    else
      LogWarn('Can''t jump to unknown screen name: ' + ScreenName);
  end;
end;

procedure TAHMUIManager.OverlayScreen(ScreenName: String);
begin
  if FCurrentScreen <> ScreenName then
  begin
    // Ensure new screen is at top of screen stack
    FScreenStack.PopScreen(ScreenName);
    FScreenStack.PushScreen(ScreenName);

    // Fire appropriate actions when we switch current screen
    HandleScreenChange(FCurrentScreen, ScreenName);

    // Store new screen name
    FCurrentScreen := ScreenName;

    // And now perform standard screen setup stuff
    InitialiseScreen;
  end;
end;

procedure TAHMUIManager.CloseScreen(ScreenName: String);
begin
  // Remove passed screen from our screen stack
  FScreenStack.PopScreen(ScreenName);

  // Change current screen to our new top screen
  FCurrentScreen := FScreenStack.GetTopScreenName;

  // Remove potentially stale item from screen history
  FScreenHistory.Closed(ScreenName);

  // Fire appropriate actions when we switch current screen
  HandleScreenChange(ScreenName, FCurrentScreen);
end;

procedure TAHMUIManager.InitialiseDescriptor;
begin
  // Initialise descriptor wide meta data
  ApplicationManagerInstance.ThemeManager.InterfaceDescriptor.MetaList.Apply;

  // Point to first screen in current descriptor
  with ApplicationManagerInstance.ThemeManager.InterfaceDescriptor do
  begin
    FScreenHistory.Clear;
    if ScreenList <> nil then
      SetCurrentScreen(ScreenList.GetDefaultLayout)
    else
      SetCurrentScreen(nil);
    FScreenHistory.Visited(FCurrentScreen);
  end;
end;

procedure TAHMUIManager.UpdateUI;
begin
  // Process required actions for our current screen
  if FCurrentScreen <> '' then
  begin
    // Initialise screen if not already done so
    if not FScreenInitialised then
    begin
      FScreenInitialised := True;
      CurrentScreen.Initialise;
    end;

    // Ask screen to invoke any necessary action events & update metadata
    CurrentScreen.UpdateActions;
    CurrentScreen.UpdateMetaValues;
  end;

  // Update media manager state
  ApplicationManagerInstance.MediaManager.UpdateMediaState;

  // Update timer engine state
  ApplicationManagerInstance.TimerManager.UpdateTimers;

  // Update animation engine state
  ApplicationManagerInstance.AnimationEngine.UpdateAnimations;

  // Update any queued actions
  ApplicationManagerInstance.ActionManager.UpdateQueuedActions;

  // Reset all layouts in case there are unsized controls on next render
  ApplicationManagerInstance.ThemeManager.InterfaceDescriptor.LayoutList.ResetLayouts;

  // Check additional rendering control settings
  FControlBorders := ApplicationManagerInstance.MetaDataManager.GetMetaBoolean(META_DEBUG_BORDERS);
  FContainerBorders := ApplicationManagerInstance.MetaDataManager.GetMetaBoolean(META_DEBUG_BORDERS);

  // Auto hide mousey if it's been idle for a while
  with TimerInstance do
    if (FMouseCursor = mcAuto) and HasDurationElapsed(FMouseHideTimer, MOUSE_HIDE_DELAY) then
      ConfigureMouse(False);
end;

procedure TAHMUIManager.RenderUI;
var
  i: Integer;
begin
  // Only perform rendering steps if screen is ready
  if ScreenInstance.BeginDrawing then
  try
    TimerInstance.UpdateFrameTiming;
    TimerInstance.BeginProfiling('Rendering UI (except diagnostics)');

    // Allow designer to render preview view instead of main UI
    if (FPreviewObject = nil) or not FPreviewObject.RenderPreview then
    begin
      // Render screen stack unless screen saver has kicked in
      if FScreenSaver.CheckIdle then
        FScreenSaver.Render
      else
      begin
        for i := 0 to Pred(FScreenStack.StackHeight) do
          FScreenStack.GetScreen(i).Render;
  //      FGestureManager.Render;
      end;
    end;

    TimerInstance.EndProfiling('Rendering UI (except diagnostics)');

    // Render designer and diagnostic overlay information if required
    if Assigned(FDesignerRenderer) then FDesignerRenderer.Render;
    FDiagnosticRenderer.Render;
  finally
    ScreenInstance.EndDrawing;
    TimerInstance.BeginProfiling('Presenting render to display device');
    ScreenInstance.UpdateDisplay;
    TimerInstance.EndProfiling('Presenting render to display device');
  end;
end;

procedure TAHMUIManager.RenderPreview(UIObject: TAHMUIObject; Handle: HWND);
var
  OldHandle: HWND;
begin
  if Assigned(UIObject) then
  begin
    // Temporarily swap screen rendering handle
    OldHandle := ScreenInstance.ScreenHandle;
    try
      // Render preview object to new handle
      ScreenInstance.ScreenHandle := Handle;
      ScreenInstance.BeginDrawing;
      try
        UIObject.RenderPreview;
      finally
        ScreenInstance.EndDrawing;
        ScreenInstance.UpdateDisplay;
      end;
    finally
      // Restore original rendering handle
      ScreenInstance.ScreenHandle := OldHandle;
    end;
  end;
end;

procedure TAHMUIManager.InvalidateUIObject(UIObject: TAHMUIObject);
var
  i: Integer;
  VisualDesc: TAHMVisualDescriptor;
  InterfaceDesc: TAHMInterfaceDescriptor;
begin
  // Ignore invalidate notification unless we're in theme designer mode
  if (not FLoading) and ApplicationManagerInstance.DesignerMode and Assigned(UIObject) then
  begin
    with ApplicationManagerInstance.ThemeManager.VisualDescriptors do
    begin
      // Some low level objects will invalidate composite object rendering
      if (UIObject is TAHMShape) or (UIObject is TAHMBitmap) or
         (UIObject is TAHMColour) then
      begin
        // Pass notification to each visual descriptor in theme cache
        for i := 0 to Pred(Count) do
        begin
          VisualDesc := TAHMVisualDescriptor(Descriptor[i]);

          // Resolve visual dependencies
          if UIObject is TAHMColour then
          begin
            VisualDesc.FontList.InvalidateUIObject(UIObject);
            VisualDesc.ShapeList.InvalidateUIObject(UIObject);
            VisualDesc.EffectList.InvalidateUIObject(UIObject);
          end
          else if UIObject is TAHMBitmap then
          begin
            VisualDesc.GridList.InvalidateUIObject(UIObject);
            VisualDesc.MediaGridList.InvalidateUIObject(UIObject);
          end
          else // TAHMShape
          begin
            VisualDesc.ButtonList.InvalidateUIObject(UIObject);
            VisualDesc.TextEditList.InvalidateUIObject(UIObject);
            VisualDesc.GridList.InvalidateUIObject(UIObject);
            VisualDesc.MediaGridList.InvalidateUIObject(UIObject);
          end;
        end;
      end;
    end;

    with ApplicationManagerInstance.ThemeManager.InterfaceDescriptors do
    begin
      // Pass notification to each interface descriptor in theme cache
      for i := 0 to Pred(Count) do
      begin
        InterfaceDesc := TAHMInterfaceDescriptor(Descriptor[i]);

        // Pass notification to all containers in the descriptor
        InterfaceDesc.ContainerList.InvalidateUIObject(UIObject);

        // Pass notification to all controls in the descriptor
        InterfaceDesc.ControlList.InvalidateUIObject(UIObject);
      end;
    end;

    // Pass notification to current screen if assigned
    if FCurrentScreen <> '' then CurrentScreen.InvalidateUIObject(UIObject);

    // Pass notification to our preview control too if assigned
    if Assigned(FPreviewControl) then FPreviewControl.InvalidateUIObject(UIObject);
  end;
end;

procedure TAHMUIManager.DeleteUIObject(UIObject: TAHMUIObject);
var
  i: Integer;
  InterfaceDesc: TAHMInterfaceDescriptor;
begin
  // Ignore delete notification unless we're in theme designer mode
  if (not FLoading) and ApplicationManagerInstance.DesignerMode and Assigned(UIObject) then
  begin
    with ApplicationManagerInstance.ThemeManager.InterfaceDescriptors do
    begin
      // Pass notification to each interface descriptor in theme cache
      for i := 0 to Pred(Count) do
      begin
        InterfaceDesc := TAHMInterfaceDescriptor(Descriptor[i]);

        // Pass notification to all containers in the descriptor
        InterfaceDesc.ContainerList.DeleteUIObject(UIObject);

        // Pass notification to all screens in the descriptor
        InterfaceDesc.ScreenList.DeleteUIObject(UIObject);
      end;
    end;
  end;
end;

function TAHMUIManager.GetPreviewControl(ControlClass: TAHMControlClass; NewInstance: Boolean): TAHMControl;
begin
  // Create a singleton preview control of the correct class
  if (FPreviewControl = nil) or (FPreviewControl.ClassType <> ControlClass) or NewInstance then
  begin
    FreeAndNil(FPreviewControl);
    FPreviewControl := ControlClass.Create(PREVIEW_CONTROL_NAME);

    // If asked to create an image control then use the screensaver bitmap
    if ControlClass = TAHMImageControl then
    begin
      if FPreviewBitmap = nil then
      begin
        FPreviewBitmap := TAHMBitmap.Create(PREVIEW_CONTROL_NAME);
        FPreviewBitmap.LoadFromURL(FResourceURL + SAVER_IMAGE_FILE);
      end;
      FPreviewControl.Style := FPreviewBitmap;
      if FPreviewPlace = nil then
      begin
        FPreviewPlace := TAHMPlace.Create(PREVIEW_CONTROL_NAME);
        FPreviewPlace.VerticalAlign := vaCenter;
        FPreviewPlace.HorizontalAlign := haCenter;
      end;
      FPreviewControl.Placement := FPreviewPlace;
    end;
  end;

  Result := FPreviewControl;
end;

function TAHMUIManager.GetControl(ControlName: String; OnScreenOnly: Boolean): TAHMControl;
var
  oScreen: TAHMScreenLayout;
  i: Integer;
begin
  // Set default result - no corresponding control
  Result := nil;

{$IFDEF DESIGNER}
  if ControlName = PREVIEW_CONTROL_NAME then
  begin
    // Return preview control for use with Animator preview
    Result := FPreviewControl;
    Exit;
  end;
{$ENDIF}

  if OnScreenOnly then
  begin
    // Iterate through screen stack in reverse order (check top first)
    for i := Pred(FScreenStack.StackHeight) downto 0 do
    begin
      oScreen := FScreenStack.GetScreen(i);
      if oScreen = nil then Break;

      // Look for requested control in this screen
      Result := oScreen.CurrentControls.GetControl(ControlName);
      if Result <> nil then Break;
    end;
  end
  else
  begin
    // Look for it in the control list of our current interface descriptor
    Result := ApplicationManagerInstance.ThemeManager.InterfaceDescriptor.ControlList.GetControl(ControlName);
  end;
end;

function TAHMUIManager.GetContainer(ContainerName: String; OnScreenOnly: Boolean): TAHMContainer;
var
  oScreen: TAHMScreenLayout;
  i: Integer;
begin
  // Set default result - no corresponding container
  Result := nil;

  if OnScreenOnly then
  begin
    // Iterate through screen stack in reverse order (check top first)
    for i := Pred(FScreenStack.StackHeight) downto 0 do
    begin
      oScreen := FScreenStack.GetScreen(i);
      if oScreen = nil then Break;

      // Look for requested container in this screen
      Result := TAHMContainer(oScreen.Contents.GetObject(ContainerName, TAHMContainer));
      if Result <> nil then Break;
    end;
  end
  else
  begin
    // Look for it in the container list of our current interface descriptor
    Result := ApplicationManagerInstance.ThemeManager.InterfaceDescriptor.ContainerList.GetContainer(ContainerName);
  end;
end;

procedure TAHMUIManager.DisplayError(ErrorText, ErrorDescription: String);
begin
  // Populate system error meta data values for text lookup
  ApplicationManagerInstance.MetaDataManager.SetMetaValue(META_ERR_TEXT, ErrorText);
  ApplicationManagerInstance.MetaDataManager.SetMetaValue(META_ERR_DESC, ErrorDescription);

  // Now invoke on error action event for UI to display error
  ApplicationManagerInstance.ActionManager.ProcessAction(AC_ONERROR);
end;

function TAHMUIManager.ProcessRemoteCommand(KeyCode: Word): Boolean;
begin
  // Ensure mediate is visible when remote key is pressed
  ScreenInstance.BringToFront;

  // Hide mousey
  ConfigureMouse(False);

  // Wake up screen saver
  ScreenSaver.DidSomething;

  // Route remote keypress directly to action manager (ignore console)
  Result := ApplicationManagerInstance.ActionManager.ProcessKeyPress(KeyCode);
end;

function TAHMUIManager.ProcessKeyPress(KeyCode: Word; Shift: TShiftState): Boolean;
begin
  // Route keypress through diagnostic console if it is active
  if FDisplayDiagnostics then
  begin
    // Try to handle keypress in console - if we do then exit here
    Result := FDiagnosticRenderer.Console.ProcessKeyPress(KeyCode, Shift);
    if Result then Exit;
  end;

  // Hide mousey
  ConfigureMouse(False);

  // Wake up screen saver
  ScreenSaver.DidSomething;

  // If preview is visible then allow it to handle keypress event
  if Assigned(FPreviewObject) then
  begin
    if FPreviewObject.IsPreviewable then FPreviewObject.PreviewKeyPress(KeyCode, Shift);
    Result := True;
    Exit;
  end;

  // If focussed control supports key presses allow it to handle event
  if Assigned(FocusControl) and FocusControl.KeyEnabled then
  begin
    FocusControl.SendKey(KeyCode, Shift);
    Result := True;
    Exit;
  end;

  // Key press not handled by diagnostic console, so pass onto action manager
  Result := ApplicationManagerInstance.ActionManager.ProcessKeyPress(KeyCode);
end;

function TAHMUIManager.CommonMouseHandler(const X, Y: Integer; Wake: Boolean): Boolean;
begin
  // Set default result - ignore mouse message
  Result := False;

  // Show mousey
  ConfigureMouse(True);

  // Scale mouse coordinates to our physical screen space
  FMouseScreenX := ScreenInstance.ScaleMouseX(X);
  FMouseScreenY := ScreenInstance.ScaleMouseY(Y);
  FMouseVirtualX := ScreenInstance.ScalePhysicalX(FMouseScreenX);
  FMouseVirtualY := ScreenInstance.ScalePhysicalY(FMouseScreenY);

  // If we want to wake the screensaver but it's idle then ignore the first
  // mouse message. If we don't want to wake and we're idle then exit
  if (Wake and ScreenSaver.DidSomething) or
     (not Wake and ScreenSaver.Idle) then Exit;

  // Okay, process this mouse message
  Result := True;
end;

function TAHMUIManager.ProcessMouseDown(X, Y: Integer): Boolean;
begin
  // Set default result - not handled
  Result := False;

  // If screensaver is running then ignore mouse message
  if ScreenSaver.Idle then Exit;

  // Perform common mouse handling
  Result := CommonMouseHandler(X, Y, False);

  // If preview is visible then allow it to handle mouse down event
  if Result and Assigned(FPreviewObject) then
  begin
    if FPreviewObject.IsPreviewable then
      FPreviewObject.PreviewMouseDown(FMouseScreenX, FMouseScreenY)
    else if (FPreviewControl is TAHMInteractiveControl) then
      with TAHMInteractiveControl(FPreviewControl) do
      begin
        Focused := BoundsPoint(FMouseScreenX, FMouseScreenY);
        MouseDown(FMouseScreenX, FMouseScreenY);
      end;
    Exit;
  end;

  // Pass event to current (topmost) screen
  if Result and (FCurrentScreen <> '') then
    CurrentScreen.MouseDown(FMouseScreenX, FMouseScreenY);

  // Pass event to gesture manager
  FGestureManager.MouseDown(FMouseScreenX, FMouseScreenY);
end;

function TAHMUIManager.ProcessMouseMove(X, Y: Integer; Down: Boolean): Boolean;
begin
  // Set default result - not handled
  Result := False;

  // Ignore this event if mouse hasn't moved at all
  if (X = FMouseLastX) and (Y = FMouseLastY) then Exit;

  // Don't wake up mouse if cursor is jittering slightly
  if (not FMouseActive) and (Abs(FMouseLastX - X) < MOUSE_JITTER_THRESHOLD)
      and (Abs(FMouseLastY - Y) < MOUSE_JITTER_THRESHOLD) then Exit;

  // Record new mouse position
  FMouseLastX := X; FMouseLastY := Y;

  // If screensaver is running then ignore mouse message
  if ScreenSaver.Idle then Exit;

  // Perform common mouse handling
  Result := CommonMouseHandler(X, Y, True);

  // If preview is visible then allow it to handle mouse move event
  if Result and Assigned(FPreviewObject) then
  begin
    if FPreviewObject.IsPreviewable then
      FPreviewObject.PreviewMouseMove(FMouseScreenX, FMouseScreenY, Down)
    else if (FPreviewControl is TAHMInteractiveControl) then
      with TAHMInteractiveControl(FPreviewControl) do
      begin
        Focused := BoundsPoint(FMouseScreenX, FMouseScreenY);
        MouseMove(FMouseScreenX, FMouseScreenY, Down);
      end;
    Exit;
  end;

  // Pass event to current (topmost) screen
  if Result and (FCurrentScreen <> '') then
    CurrentScreen.MouseMove(FMouseScreenX, FMouseScreenY, Down);

  // Pass event to gesture manager
  FGestureManager.MouseMove(FMouseScreenX, FMouseScreenY, Down);
end;

function TAHMUIManager.ProcessMouseUp(X, Y: Integer): Boolean;
begin
  // Perform common mouse handling
  Result := CommonMouseHandler(X, Y, True);

  // If preview is visible then allow it to handle mouse up event
  if Result and Assigned(FPreviewObject) then
  begin
    if FPreviewObject.IsPreviewable then
      FPreviewObject.PreviewMouseUp(FMouseScreenX, FMouseScreenY)
    else if (FPreviewControl is TAHMInteractiveControl) then
      with TAHMInteractiveControl(FPreviewControl) do
      begin
        Focused := BoundsPoint(FMouseScreenX, FMouseScreenY);
        MouseUp(FMouseScreenX, FMouseScreenY);
      end;
    Exit;
  end;

  // Pass event to current (topmost) screen
  if Result and (FCurrentScreen <> '') then
    CurrentScreen.MouseUp(FMouseScreenX, FMouseScreenY);

  // Pass event to gesture manager
  FGestureManager.MouseUp(FMouseScreenX, FMouseScreenY);
end;

function TAHMUIManager.ProcessMouseWheel(Delta: Integer): Boolean;
begin
  // Show mousey
  ConfigureMouse(True);

  // Wake up screen saver
  ScreenSaver.DidSomething;

  // Event handled
  Result := True;

  // If preview is visible then allow it to handle mouse wheel event
  if Assigned(FPreviewObject) then
  begin
    if FPreviewObject.IsPreviewable then FPreviewObject.PreviewMouseWheel(Delta)
    else if (FPreviewControl is TAHMInteractiveControl) then
      with TAHMInteractiveControl(FPreviewControl) do MouseWheel(Delta);
    Exit;
  end;

  // Pass event to current (topmost) screen
  if FCurrentScreen <> '' then
    CurrentScreen.MouseWheel(Delta);
end;

function TAHMUIManager.UpdateMousePosition(X, Y: Integer): Boolean;
begin
  // Perform common mouse handling, but no UI interraction
  Result := CommonMouseHandler(X, Y, True);
end;

function TAHMUIManager.RemoveAbstractControl(Remove: TAHMAbstractControl): TAHMUIObject;
begin
  // Set default result - no abstract control found
  Result := nil;

  if FPreviewObject is TAHMContainer then
    Result := TAHMContainer(FPreviewObject).RemoveAbstractControl(Remove)
  else if FPreviewObject is TAHMScreenLayout then
    Result := TAHMScreenLayout(FPreviewObject).RemoveAbstractControl(Remove)
  else if FCurrentScreen <> '' then
    Result := CurrentScreen.RemoveAbstractControl(Remove);
end;

function TAHMUIManager.FindAbstractControl(X, Y: Integer; Ignore: TAHMAbstractControl): TAHMAbstractControl;
begin
  // Set default result - no abstract control found
  Result := nil;

  // Look for abstract control in current (topmost) screen or just use preview
  if CommonMouseHandler(X, Y, True) then
    if FPreviewObject is TAHMControl then
      Result := TAHMControl(FPreviewObject)
    else if FPreviewObject is TAHMContainer then
      Result := TAHMContainer(FPreviewObject).FindAbstractControl(FMouseScreenX, FMouseScreenY, Ignore)
    else if FPreviewObject is TAHMScreenLayout then
      Result := TAHMScreenLayout(FPreviewObject).FindAbstractControl(FMouseScreenX, FMouseScreenY, Ignore)
    else if FCurrentScreen <> '' then
      Result := CurrentScreen.FindAbstractControl(FMouseScreenX, FMouseScreenY, Ignore);
end;

function TAHMUIManager.FindControl(X, Y: Integer; Ignore: TAHMControl): TAHMControl;
begin
  // Set default result - no control found
  Result := nil;

  // Look for control in current (topmost) screen or just use preview
  if CommonMouseHandler(X, Y, True) then
    if FPreviewObject is TAHMControl then
      Result := TAHMControl(FPreviewObject)
    else if FPreviewObject is TAHMContainer then
      Result := TAHMContainer(FPreviewObject).FindControl(FMouseScreenX, FMouseScreenY, Ignore)
    else if FPreviewObject is TAHMScreenLayout then
      Result := TAHMScreenLayout(FPreviewObject).FindControl(FMouseScreenX, FMouseScreenY, Ignore)
    else if FCurrentScreen <> '' then
      Result := CurrentScreen.FindControl(FMouseScreenX, FMouseScreenY, Ignore);
end;

function TAHMUIManager.FindContainer(X, Y: Integer; Ignore: TAHMContainer): TAHMContainer;
begin
  // Set default result - no container found
  Result := nil;

  // Look for container in current (topmost) screen or just use preview
  if CommonMouseHandler(X, Y, True) then
    if FPreviewObject is TAHMContainer then
      Result := TAHMContainer(FPreviewObject)
    else if FPreviewObject is TAHMScreenLayout then
      Result := TAHMScreenLayout(FPreviewObject).FindContainer(FMouseScreenX, FMouseScreenY, Ignore)
    else if FCurrentScreen <> '' then
      Result := CurrentScreen.FindContainer(FMouseScreenX, FMouseScreenY, Ignore);
end;

function TAHMUIManager.FindScreen(X, Y: Integer; Ignore: TAHMScreenLayout): TAHMScreenLayout;
begin
  // Set default result - no screen found
  Result := nil;

  // Look for screen in screen stack or just use preview
  if CommonMouseHandler(X, Y, True) then
    if FPreviewObject is TAHMScreenLayout then
      Result := TAHMScreenLayout(FPreviewObject)
    else
      Result := FScreenStack.FindScreen(Ignore);
end;


end.


